"""The Squeezebox integration."""

import asyncio
from asyncio import timeout
from dataclasses import dataclass, field
from datetime import datetime
from http import HTTPStatus
import logging

from pysqueezebox import Player, Server

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
    CONF_PORT,
    CONF_USERNAME,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
    ConfigEntryAuthFailed,
    ConfigEntryError,
    ConfigEntryNotReady,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import (
    CONNECTION_NETWORK_MAC,
    DeviceEntryType,
    format_mac,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.util.hass_dict import HassKey

from .const import (
    CONF_HTTPS,
    DISCOVERY_INTERVAL,
    DOMAIN,
    SERVER_MANUFACTURER,
    SERVER_MODEL,
    SERVER_MODEL_ID,
    SIGNAL_PLAYER_DISCOVERED,
    SIGNAL_PLAYER_REDISCOVERED,
    STATUS_API_TIMEOUT,
    STATUS_QUERY_LIBRARYNAME,
    STATUS_QUERY_MAC,
    STATUS_QUERY_UUID,
    STATUS_QUERY_VERSION,
)
from .coordinator import (
    LMSStatusDataUpdateCoordinator,
    SqueezeBoxPlayerUpdateCoordinator,
)

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [
    Platform.BINARY_SENSOR,
    Platform.BUTTON,
    Platform.MEDIA_PLAYER,
    Platform.SENSOR,
    Platform.SWITCH,
    Platform.UPDATE,
]

SQUEEZEBOX_HASS_DATA: HassKey[asyncio.Task] = HassKey(DOMAIN)


@dataclass
class SqueezeboxData:
    """SqueezeboxData data class."""

    coordinator: LMSStatusDataUpdateCoordinator
    server: Server
    known_player_ids: set[str] = field(default_factory=set)


type SqueezeboxConfigEntry = ConfigEntry[SqueezeboxData]


async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -> bool:
    """Set up an LMS Server from a config entry."""
    config = entry.data
    session = async_get_clientsession(hass)
    _LOGGER.debug(
        "Reached async_setup_entry for host=%s(%s)", config[CONF_HOST], entry.entry_id
    )

    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)
    https = config.get(CONF_HTTPS, False)
    host = config[CONF_HOST]
    port = config[CONF_PORT]

    lms = Server(session, host, port, username, password, https=https)
    _LOGGER.debug("LMS object for %s", lms)

    try:
        async with timeout(STATUS_API_TIMEOUT):
            status = await lms.async_query(
                "serverstatus", "-", "-", "prefs:libraryname"
            )
    except TimeoutError as err:  # Specifically catch timeout
        _LOGGER.warning("Timeout connecting to LMS %s: %s", host, err)
        raise ConfigEntryNotReady(
            translation_domain=DOMAIN,
            translation_key="init_timeout",
            translation_placeholders={
                "host": str(host),
            },
        ) from err

    if not status:
        # pysqueezebox's async_query returns None on various issues,
        # including HTTP errors where it sets lms.http_status.

        if lms.http_status == HTTPStatus.UNAUTHORIZED:
            _LOGGER.warning("Authentication failed for Squeezebox server %s", host)
            raise ConfigEntryAuthFailed(
                translation_domain=DOMAIN,
                translation_key="init_auth_failed",
                translation_placeholders={
                    "host": str(host),
                },
            )

        # For other errors where status is None (e.g., server error, connection refused by server)
        _LOGGER.warning(
            "LMS %s returned no status or an error (HTTP status: %s). Retrying setup",
            host,
            lms.http_status,
        )
        raise ConfigEntryNotReady(
            translation_domain=DOMAIN,
            translation_key="init_get_status_failed",
            translation_placeholders={
                "host": str(host),
                "http_status": str(lms.http_status),
            },
        )

    # If we are here, status is a valid dictionary
    _LOGGER.debug("LMS Status for setup  = %s", status)

    # Check for essential keys in status before using them
    if STATUS_QUERY_UUID not in status:
        _LOGGER.error("LMS %s status response missing UUID", host)
        # This is a non-recoverable error with the current server response
        raise ConfigEntryError(
            translation_domain=DOMAIN,
            translation_key="init_missing_uuid",
            translation_placeholders={
                "host": str(host),
            },
        )

    lms.uuid = status[STATUS_QUERY_UUID]
    _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid)
    lms.name = (
        (STATUS_QUERY_LIBRARYNAME in status and status[STATUS_QUERY_LIBRARYNAME])
        and status[STATUS_QUERY_LIBRARYNAME]
    ) or host
    version = (STATUS_QUERY_VERSION in status and status[STATUS_QUERY_VERSION]) or None
    # mac can be missing
    mac_connect = (
        {(CONNECTION_NETWORK_MAC, format_mac(status[STATUS_QUERY_MAC]))}
        if STATUS_QUERY_MAC in status
        else None
    )

    device_registry = dr.async_get(hass)
    device = device_registry.async_get_or_create(
        config_entry_id=entry.entry_id,
        identifiers={(DOMAIN, lms.uuid)},
        name=lms.name,
        manufacturer=SERVER_MANUFACTURER,
        model=SERVER_MODEL,
        model_id=SERVER_MODEL_ID,
        sw_version=version,
        entry_type=DeviceEntryType.SERVICE,
        connections=mac_connect,
    )
    _LOGGER.debug("LMS Device %s", device)

    server_coordinator = LMSStatusDataUpdateCoordinator(hass, entry, lms)

    entry.runtime_data = SqueezeboxData(coordinator=server_coordinator, server=lms)

    async def _player_discovery(now: datetime | None = None) -> None:
        """Discover squeezebox players by polling server."""

        async def _discovered_player(player: Player) -> None:
            """Handle a (re)discovered player."""
            if player.player_id in entry.runtime_data.known_player_ids:
                await player.async_update()
                async_dispatcher_send(
                    hass,
                    SIGNAL_PLAYER_REDISCOVERED + entry.entry_id,
                    player.player_id,
                    player.connected,
                )
            else:
                _LOGGER.debug("Adding new entity: %s", player)
                player_coordinator = SqueezeBoxPlayerUpdateCoordinator(
                    hass, entry, player, lms.uuid
                )
                await player_coordinator.async_refresh()
                entry.runtime_data.known_player_ids.add(player.player_id)
                async_dispatcher_send(
                    hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, player_coordinator
                )

        if players := await lms.async_get_players():
            for player in players:
                hass.async_create_task(_discovered_player(player))

        entry.async_on_unload(
            async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
        )

    await server_coordinator.async_config_entry_first_refresh()
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    _LOGGER.debug(
        "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
    )
    entry.async_create_background_task(
        hass, _player_discovery(), "squeezebox.media_player.player_discovery"
    )

    return True


async def async_unload_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -> bool:
    """Unload a config entry."""
    # Stop player discovery task for this config entry.
    _LOGGER.debug(
        "Reached async_unload_entry for LMS=%s(%s)",
        entry.runtime_data.server.name or "Unknown",
        entry.entry_id,
    )

    # Stop server discovery task if this is the last config entry.
    current_entries = hass.config_entries.async_entries(DOMAIN)
    if len(current_entries) == 1 and current_entries[0] == entry:
        _LOGGER.debug("Stopping server discovery task")
        hass.data[SQUEEZEBOX_HASS_DATA].cancel()
        hass.data.pop(SQUEEZEBOX_HASS_DATA)

    return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
